vue-router源码分析(base.js)

base.js

src/history/base.js

定义了History类,VueRouter中的history,根据mode,可能是HTML5History、HashHistory或Abstract实例,其中HTML5History、HashHistory等都是继承自History类。

History类提供了一些路由操作的基本方法:

  1. Listen——监听callback

  2. onReady ——监听路由是否ready,ready时,将所有cb装进readyCbs列表

  3. onError

  4. transitionTo ——路由的跳转,会判断跳转to的路径是否在路由表中:是,才进行组件替换,调用confirmTransition

1. constructor构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
constructor(router: Router, base: ?string) {
// 获取当前router
this.router = router;
// 获取路由base
this.base = normalizeBase(base);
// 由createRoute生成的基础路由,path:'/'
this.current = START;
this.pending = null;
this.ready = false;
this.readyCbs = [];
this.readyErrorCbs = [];
this.errorCbs = [];
}

2. transitionTo路由跳转函数

路由的跳转,会判断跳转to的路径是否在路由表中:是,才进行组件替换,调用confirmTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
transitionTo(
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
// 调用 match 得到匹配的 route 对象
const route = this.router.match(location, this.current);

// 确认过渡
this.confirmTransition(
route,
() => {
// 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
// 调用 afterHooks 中的钩子函数
this.updateRoute(route);
// 添加 hashchange 监听
onComplete && onComplete(route);
// 子类实现的更新url地址
// 对于 hash 模式的话 就是更新 hash 的值
// 对于 history 模式的话 就是利用 pushstate / replacestate 来更新
// 更新 URL
this.ensureURL();

// 只执行一次 ready 回调
if (!this.ready) {
this.ready = true;
this.readyCbs.forEach(cb => {
cb(route);
});
}
},
err => {
// 错误处理
if (onAbort) {
onAbort(err);
}
if (err && !this.ready) {
this.ready = true;
this.readyErrorCbs.forEach(cb => {
cb(err);
});
}
}
);
}

match路由匹配函数

src/create-matcher.js

先看如何匹配路由获得路由信息:判断两步,命名路由,非命名路由,因为带有params只能用命名路由引入,非命名路由不用判断parmas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 序列化 url
// 比如对于该 url 来说 /abc?foo=bar&baz=qux#hello
// 会序列化路径为 /abc
// 哈希为 #hello
// 参数为 foo: 'bar', baz: 'qux'
const location = normalizeLocation(raw, currentRoute, false, router);
const { name } = location;

// 如果是命名路由,就判断记录中是否有该命名路由配置
if (name) {
const record = nameMap[name];
if (process.env.NODE_ENV !== "production") {
warn(record, `Route with name '${name}' does not exist`);
}
// 没找到表示没有匹配的路由
if (!record) return _createRoute(null, location);
// 获取所有必须的params。如果optional为true说明params不是必须的
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name);

// 参数处理
if (typeof location.params !== "object") {
location.params = {};
}

if (currentRoute && typeof currentRoute.params === "object") {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}

location.path = fillParams(
record.path,
location.params,
`named route "${name}"`
);
return _createRoute(record, location, redirectedFrom);
} else if (location.path) {
// 非命名路由处理
location.params = {};
for (let i = 0; i < pathList.length; i++) {
// 查找记录
const path = pathList[i];
const record = pathMap[path];
// 如果匹配路由,则创建路由
// pathMap[path] = 路由记录
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom);
}
}
}
// 没有匹配的路由
return _createRoute(null, location);
}

_createRoute

根据上面match函数条件的不同创建不同的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根据条件创建不同的路由
function _createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 重定向和别名逻辑
if (record && record.redirect) {
return redirect(record, redirectedFrom || location);
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs);
}
// 创建路由对象
return createRoute(record, location, redirectedFrom, router);
}

createRoute创建路由对象

src/util/route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export function createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery;

// 克隆参数
let query: any = location.query || {};
try {
query = clone(query);
} catch (e) {}

// 创建路由对象
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || "/",
hash: location.hash || "",
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
// 根据记录层级的得到所有匹配的 路由记录
matched: record ? formatMatch(record) : []
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
}
// 让路由对象不可修改
return Object.freeze(route);
}

// 获得包含当前路由的所有嵌套路径片段的路由记录
// 包含从根路由到当前路由的匹配记录,从上至下
function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
const res = [];
while (record) {
res.unshift(record);
record = record.parent;
}
return res;
}

3. confirmTransition确认过渡函数

负责控制所有的路由守卫的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current;

// 中断跳转路由函数
const abort = err => {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err);
});
} else {
warn(false, "uncaught error during route navigation:");
console.error(err);
}
}
onAbort && onAbort(err);
};

// 如果是相同 直接返回
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(new NavigationDuplicated(route));
}

// 下面分析
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
);

// 下面分析
const queue: Array<?NavigationGuard> = [].concat(...);

// 下面分析
const iterator = (hook: NavigationGuard, next) => {
// ...
};

// 下面分析
runQueue(queue, iterator, () => {
// ...
});
}

路由守卫的原理

和组件的生命周期的钩子不同,路由守卫将重点放在路由上,能够控制路由跳转,一般用在页面级别的路由跳转时控制跳转的逻辑,比如在路由守卫中检查用户是否有进入当前页面的权限,没有则跳转到授权页面,亦或是在离开页面时警告用户有未确认的信息,确认后才能跳转等等

在路由守卫中,一般会接收3个参数,to,from,next,前两个分别是跳转后和跳转前页面路由的 $route 对象,第三个参数 next 是一个函数,当执行 next 函数后会进行跳转,如果一个包含 next 参数的路由守卫里没有执行该函数,页面会无法跳转

resolveQueue函数 获取所有需要激活、更新、销毁的路由

1
2
3
4
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
);

根据跳转前和跳转后的route对象的matched数组(当前 $route 对象以及所有父级的路由记录),返回这2个数组包含的路由记录的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function resolveQueue(
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i;
const max = Math.max(current.length, next.length);
for (i = 0; i < max; i++) {
// 当前路由路径和跳转路由路径不同时跳出遍历
if (current[i] !== next[i]) {
break;
}
}
return {
// 可复用的组件对应路由
updated: next.slice(0, i),
// 需要渲染的组件对应路由
activated: next.slice(i),
// 失活的组件对应路由
deactivated: current.slice(i)
};
}

queue函数 获取所有需要执行的路由守卫

数组中的守卫排列顺序是设计好的,对应vue-router官方文档中提到的路由导航解析流程

  1. 导航被触发
  2. 在失活的组件里调用离开守卫
  3. 调用全局的beforeEach守卫
  4. 在重用的组件里调用beforeRouteUpdate守卫(2.2+)
  5. 在路由配置里调用beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用beforeRouteEnter
  8. 调用全局的beforeResolve守卫(2.5)
  9. 导航被确认
  10. 调用全局的afterEach钩子
  11. 触发DOM更新
  12. 用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* NavigationGuard是一个标准的路由守卫的签名,经过 queue 数组内部这些函数的转换最终会返回路由守卫组成的数组
declare type NavigationGuard = (
to: Route,
from: Route,
next: (to?: RawLocation | false | Function | void) => void
) => any
*/
const queue: Array<?NavigationGuard> = [].concat(
// 返回离开组件的 beforeRouteLeave 钩子函数 (数组:子 => 父)
extractLeaveGuards(deactivated),
// 返回路由实例(全局)的 beforeEach 钩子函数 (数组)
this.router.beforeHooks,
// 返回当前组件的 beforeRouteUpdate 钩子函数 (数组:父 => 子)
extractUpdateHooks(updated),
// 返回当前组件的 beforeEnter 钩子函数 (数组)
activated.map(m => m.beforeEnter),
// 解析异步路由组件(同样会返回一个导航守卫函数的签名,但是用不到 to,from 这两个参数)
resolveAsyncComponents(activated)
);

分析:extractLeaveGuards执行函数

先分析queue数组里的第一个执行函数extractLeaveGuards,经过一层封装,最终会执行通用函数extractGuards

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
** @records 删除的路由记录
** @name beforeRouteLeave,即最终触发的是beforeRouteLeave守卫
*/
function extractGuards(
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
/*
** @def 视图名对应的组件配置项(因为 vue-router 支持命名视图所以可能会有多个视图名,大部分情况为 default,及使用默认视图),当是异步路由时,def为异步返回路由的函数
** @instance 组件实例
** @match 当前遍历到的路由记录
** @key 视图名
*/
const guards = flatMapComponents(records, (def, instance, match, key) => {
// 找出组件中对应的钩子函数
const guard = extractGuard(def, name);
if (guard) {
// 给每个钩子函数添加上下文对象为组件自身
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key);
}
});
// 数组降维,并且判断是否需要翻转数组
// 因为某些钩子函数需要从子执行到父
return flatten(reverse ? guards.reverse() : guards);
}

flatMapComponents通用函数:遍历records数组,每次执行第二个回调函数,类似于数组的map方法

在回调函数内部会执行extractGuard函数

1
2
3
4
5
6
7
8
9
10
function extractGuard(
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== "function") {
// extend now so that global mixins are applied.
def = _Vue.extend(def);
}
return def.options[key];
}

def为组件配置项,通过Vue核心库的函数extend将配置项转为组件构造器(虽然配置项中就能拿到对应的路由守卫,但是官方注释只有转为构造器后才能拿到一些全局混入的钩子),在生成构造器时,Vue会将配置项赋值给构造器的静态属性options,最后返回配置项中对应的路由守卫函数,即如果我们在跳转后的组件中定义了beforeRouteLeave 的话这里就会返回这个函数

最后拿到返回值guard后会经过一层处理,例如扁平化,绑定this指向;根据reverse参数决定是否要反转数组(因为matched中路由记录顺序是父=》子,而beforeRouteLeave 需要从最里层子组件触发,所以需要进行反转保证守卫触发顺序)

分析:resolveAsyncComponents解析异步组件

异步组件:通俗来说,是指使用路由懒加载返回的路由,我们可以使用import()去动态加载 JS 文件,放到 vue-router 中,实现异步加载组件配置项 component: ()=>import('./components/comp1)

resolveAsyncComponents函数最终会返回一个函数,并且符合路由守卫的函数签名(可能只是为了保证返回函数的一致性,实质上在这个函数中,并不会用到 to from 这两个参数)

这个函数只是被定义了,并没有执行

首先通过flatMapComponents遍历新增的路由记录,每次遍历都执行第二个回调函数

在回调函数里,会定义一个resolve函数,当异步组件加载完成后,会通过 then 的形式解析 promise,最终会调用 resolve函数并传入异步组件的配置项作为参数,resolve函数接收到组件配置项后会像 Vue 中一样将配置项转为构造器,同时将值赋值给当前路由记录的 components 属性中(key 属性默认为 default)

另外resolveAsyncComponents函数会通过闭包保存一个 pending 变量:代表接收的异步组件数量,在flatMapComponents遍历的过程中,每次会将 pending 加一,而当异步组件被解析完毕后,再将 pending 减一,当 pending 为0时,代表异步组件全部解析完成,随即执行 next 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export function resolveAsyncComponents(matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false;
let pending = 0;
let error = null;

flatMapComponents(matched, (def, _, match, key) => {
// vue-router 没使用 Vue 核心库解析异步组件的函数,原因是希望能够实现停止路由跳转知道懒加载的组件被解析成功

// 判断是否是异步组件
if (typeof def === "function" && def.cid === undefined) {
hasAsync = true;
pending++;

// 成功回调
// once 函数确保异步组件只加载一次
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default;
}
// 判断是否是构造函数
// 不是的话通过 Vue 来生成组件构造函数
def.resolved =
typeof resolvedDef === "function"
? resolvedDef
: _Vue.extend(resolvedDef);
// 赋值组件
// 如果组件全部解析完毕,继续下一步
match.components[key] = resolvedDef;
pending--;
if (pending <= 0) {
next();
}
});

// 失败回调
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`;
process.env.NODE_ENV !== "production" && warn(false, msg);
if (!error) {
error = isError(reason) ? reason : new Error(msg);
next(error);
}
});

let res;
try {
// 执行异步组件函数
res = def(resolve, reject);
} catch (e) {
reject(e);
}
if (res) {
// 下载完成执行回调
if (typeof res.then === "function") {
res.then(resolve, reject);
} else {
// new syntax in Vue 2.3
const comp = res.component;
if (comp && typeof comp.then === "function") {
comp.then(resolve, reject);
}
}
}
}
});

// 不是异步组件直接下一步
if (!hasAsync) next();
};
}